Skip to content

fix(plugin-sharing): share-link enforcement 吃完整授权信封,修复 group 姿态建链恒 403 (#6206) - #6552

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-6206-sharelink-full-context
Aug 8, 2026
Merged

fix(plugin-sharing): share-link enforcement 吃完整授权信封,修复 group 姿态建链恒 403 (#6206)#6552
baozhoutao merged 1 commit into
mainfrom
claude/issue-6206-sharelink-full-context

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #6206

裁决 A 案(维护者 2026-08-07,comment 5219847367)的消费半边。契约半边已随 PR #6511(d7e0b4212,#6430)落 main,本 PR 按落地后的契约形状消费。

问题

share-link 路由在 resolveAuthzContext 之后重新拼了一个四字段对象:

return { userId: authz.userId, tenantId: authz.tenantId, positions: authz.positions, permissions: authz.permissions };

这个对象不是路由自己用的身份 —— 它被原样交给 ShareLinkService.createLink,而后者用它去 engine.find 复读目标记录([Finding-2]「只能为自己看得见的记录建链接」)。于是 accessible_org_idsorg_user_idssystemPermissionsposturetabPermissions 在进 enforcement 的路上被丢掉

group 租户姿态下 accessible_org_ids 就是 Layer 0 那堵墙(ADR-0105 D2),集合缺席即判否(fail closed)⇒ 可见性校验查不到任何行 ⇒ 路由抛 403。是可用性缺陷,不是泄露。

改法

信封整个透传,不再逐字段挑选:

return { ...authz, isSystem: false };

逐字段挑选正是这条缝出问题的方式 —— 换一份「这次写全了」的字段清单,只是把同一颗雷推迟到 ResolvedAuthzContext 下次长出新维度的那天。posture 因此随上下文流动,不在 enforcement 处重推(ADR-0095 D2)。isSystem: false 与两个兄弟传输面(rest-server.ts / runtime/src/security/resolve-execution-context.ts)写法一致,也就是 ADR-0118 D2 的「absence is never system」显式化。

窄类型 ShareLinkExecutionContext 按裁决保留,但只服务路由自己的 401 判定:新增的 isAuthenticated(ctx) 是它在本仓库唯一的消费点 —— 在读处收窄,而不是在生产处收窄,这正是裁决的要点。share-link-service.ts 三个裁决方法与 canManageShares 探针的形参随 #6511 收成完整 ExecutionContext

before-red:分诊的 repro 义务已兑现(复现成立)

分诊 caveat 说得很明白:方向系代码读出、未跑复现,接手第一件事是先复现。已跑,方向属实。

新增 packages/plugins/plugin-security/src/share-link-tenant-wall.test.ts —— 放在 plugin-security 是因为判否的那一步在这里算(computeTenantLayer0Filter),而该包本来就 devDepend 了 plugin-sharing(controlled-by-parent-sharing.test.ts 等先例),反向依赖则会成环。测试里真实的部分是:被 boot 的真插件(所以跑的是生产那条闭包)、真 share-link service、真墙;替身只有存储 —— 且 RLS_DENY_FILTER 是靠「匹配不到任何行」判否的,和生产同一机制,不是特判。

把裁剪恢复回去、重建 dist 后:

× mints a link for a record the caller can read ... AssertionError: expected 403 to be 201
× reaches records across EVERY organization the caller belongs to (MOAC union) ... expected 403 to be 201
  Test Files  1 failed (1)   Tests  2 failed | 2 passed (4)

修后 4/4 绿。两条在两个世界里都绿的用例是防自欺的:single 姿态 201 不变(墙在该姿态惰性),以及调用方不属于记录所在组织时依然 403 —— 变宽的是信封,不是权限。

回归钉

packages/plugins/plugin-sharing/src/share-link-enforcement-context.test.ts 不写死字段清单 —— 写死清单正是坏掉的那件工件。它拿 #5859 的 seam kit(bootRequestContext,走真 resolveAuthzContext)为同一 principal 造一份参照信封,断言 enforcement 拿到的 context 不缺任何一个键。将来新增授权维度两边同时覆盖;有人再裁一刀,报错会把丢掉的键名列出来:

+   "systemPermissions", "org_user_ids", "accessible_org_ids", "accessToken", "email", "posture", "isSystem",

外加卡片点名的四个键按名断言、401 逻辑不变(且 401 在任何 enforcement 之前判定)、以及看不见的记录仍旧 403。

验证

  • pnpm --filter @objectstack/plugin-sharing test → 14 files / 363 tests 全绿
  • pnpm --filter @objectstack/plugin-security test → 36 files / 773 tests 全绿
  • pnpm --filter @objectstack/runtime test(消费半径:dispatcher 面的 share-links domain)→ 110 files / 1606 tests 全绿
  • typecheck:plugin-sharing / plugin-security / runtime 三包 Done
  • 门禁:check:engine-double-contract(两个新替身的 update/delete 均以 assertEngineUpdateDispatch / assertEngineDeleteDispatch 开场)、check:authz-resolvercheck:route-envelopecheck:adr-anchorscheck:slot-lookupcheck:error-code-casingcheck:nul-bytes 全绿

范围外发现

⛔ 未触碰 sharing-plugin.ts :857 一带的 FORBIDDEN 抛点(#5493 领地);未动 spec。


Generated by Claude Code

…elope (#6206)

The share-link routes rebuilt a four-field object out of the
`resolveAuthzContext` result (`userId`/`tenantId`/`positions`/`permissions`)
and handed it straight to `engine.find` as the [Finding-2] visibility check's
context. `accessible_org_ids`, `org_user_ids`, `systemPermissions`, `posture`
and `tabPermissions` were dropped on the way into enforcement.

Under the `group` tenancy posture `accessible_org_ids` IS the Layer 0 wall
(ADR-0105 D2) and an absent set denies, so the check failed closed and link
creation answered 403 for records the caller reads fine elsewhere — reproduced
here, not only read from the code.

The envelope is now passed through whole (`{ ...authz, isSystem: false }`),
per the maintainer's option-A ruling on #6206 and the contract half that landed
with #6511. `posture` travels with the context and is never re-derived at the
enforcement site (ADR-0095 D2). `ShareLinkExecutionContext` survives as the
routes' own 401 vocabulary, consumed only by the new `isAuthenticated` gate.

Tests: a seam-parity pin in plugin-sharing (the enforcement context must carry
every key the real resolver produced — re-trimming fails by naming the dropped
keys) and the behavioural `group`-posture repro in plugin-security, which owns
`computeTenantLayer0Filter` and can therefore drive the real wall.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 4:25am

Request Review

@github-actions github-actions Bot added the size/l label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/plugin-sharing)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/plugin-sharing)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-sharing)
  • content/docs/permissions/authorization.mdx (via packages/plugins/plugin-sharing)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-sharing)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-sharing)
  • content/docs/protocol/objectql/security.mdx (via packages/plugins/plugin-sharing)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-sharing)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants